﻿using GitCommands.Config;
using GitExtensions.Extensibility;
using GitExtensions.Extensibility.Configurations;
using GitExtensions.Extensibility.Git;
using Microsoft;

namespace GitCommands.Remotes
{
    public interface IConfigFileRemoteSettingsManager
    {
        void ConfigureRemotes(string remoteName);

        /// <summary>
        /// Returns the default remote for push operation.
        /// </summary>
        /// <returns>The <see cref="GitRef.Name"/> if found; otherwise, <see langword="null"/>.</returns>
        string? GetDefaultPushRemote(ConfigFileRemote remote, string branch);

        /// <summary>
        /// Loads the list of remotes configured in .git/config file.
        /// </summary>
        IEnumerable<ConfigFileRemote> LoadRemotes(bool loadDisabled);

        /// <summary>
        /// Removes the specified remote from .git/config file.
        /// </summary>
        /// <param name="remote">Remote to remove.</param>
        /// <returns>Output of the operation.</returns>
        string RemoveRemote(ConfigFileRemote remote);

        /// <summary>
        /// Returns true if input remote exists and is enabled.
        /// </summary>
        /// <param name="remoteName">Name of remote to check.</param>
        /// <returns><see langword="true"/> if input remote exists and is enabled; otherwise, <see langword="false"/>.</returns>
        bool EnabledRemoteExists(string remoteName);

        /// <summary>
        /// Returns true if input remote exists and is disabled.
        /// </summary>
        /// <param name="remoteName">Name of remote to check.</param>
        /// <returns><see langword="true"/> if input remote exists and is disabled; otherwise, <see langword="false"/>.</returns>
        bool DisabledRemoteExists(string remoteName);

        /// <summary>
        ///   Saves the remote details by creating a new or updating an existing remote entry in .git/config file.
        /// </summary>
        /// <param name="remote">An existing remote instance or <see langword="null"/> if creating a new entry.</param>
        /// <param name="remoteName">
        ///   <para>The remote name.</para>
        ///   <para>If updating an existing remote and the name changed, it will result in remote name change and prompt for "remote update".</para>
        /// </param>
        /// <param name="remoteUrl">
        ///   <para>The remote URL.</para>
        ///   <para>If updating an existing remote and the URL changed, it will result in remote URL change and prompt for "remote update".</para>
        /// </param>
        /// <param name="remotePushUrl">An optional alternative remote push URL.</param>
        /// <param name="remotePuttySshKey">An optional Putty SSH key.</param>
        /// <param name="remoteColor">An optional color for the remote.</param>
        /// <returns>Result of the operation.</returns>
        ConfigFileRemoteSaveResult SaveRemote(ConfigFileRemote? remote, string remoteName, string remoteUrl, string? remotePushUrl, string remotePuttySshKey, string? remoteColor);

        /// <summary>
        ///  Marks the remote as enabled or disabled in .git/config file.
        /// </summary>
        /// <param name="remoteName">An existing remote instance.</param>
        /// <param name="disabled">The new state of the remote. <see langword="true"/> to disable the remote; otherwise <see langword="false"/>.</param>
        void ToggleRemoteState(string remoteName, bool disabled);

        /// <summary>
        /// Retrieves disabled remotes from .git/config file.
        /// </summary>
        IReadOnlyList<Remote> GetDisabledRemotes();

        /// <summary>
        /// Retrieves disabled remote names from the .git/config file.
        /// </summary>
        IReadOnlyList<string> GetDisabledRemoteNames();

        /// <summary>
        /// Retrieves enabled remote names.
        /// </summary>
        IReadOnlyList<string> GetEnabledRemoteNames();
    }

    public class ConfigFileRemoteSettingsManager : IConfigFileRemoteSettingsManager
    {
        internal static readonly string DisabledSectionPrefix = "-";
        internal static readonly string SectionRemote = "remote";
        internal static readonly string SectionRemoteDisabled = $"{DisabledSectionPrefix}{SectionRemote}";
        private readonly Func<IGitModule?> _getModule;

        public ConfigFileRemoteSettingsManager(Func<IGitModule?> getModule)
        {
            _getModule = getModule;
        }

        // TODO: moved verbatim from FormRemotes.cs, perhaps needs refactoring
        public void ConfigureRemotes(string remoteName)
        {
            IGitModule module = GetModule();
            IReadOnlyList<IGitRef> moduleRefs = module.GetRefs(RefsFilter.Heads);

            foreach (IGitRef remoteHead in moduleRefs)
            {
                if (!remoteHead.IsRemote ||
                    !remoteHead.Name.Contains(remoteName, StringComparison.InvariantCultureIgnoreCase))
                {
                    continue;
                }

                foreach (IGitRef localHead in moduleRefs)
                {
                    if (localHead.IsRemote ||
                        !string.IsNullOrEmpty(localHead.TrackingRemote) ||
                        !remoteHead.Name.Contains(localHead.Name, StringComparison.InvariantCultureIgnoreCase))
                    {
                        continue;
                    }

                    localHead.TrackingRemote = remoteName;
                    localHead.MergeWith = localHead.Name;
                }
            }
        }

        /// <summary>
        /// Returns the default remote for push operation.
        /// </summary>
        /// <returns>The <see cref="GitRef.Name"/> if found, otherwise <see langword="null"/>.</returns>
        // TODO: moved verbatim from FormPush.cs, perhaps needs refactoring
        public string? GetDefaultPushRemote(ConfigFileRemote remote, string branch)
        {
            ArgumentNullException.ThrowIfNull(remote);

            IGitModule module = GetModule();

            GitRef remoteHead = remote.Push
                                   .Select(s => s.Split(Delimiters.Colon))
                                   .Where(t => t.Length == 2)
                                   .Where(t => IsSettingForBranch(t[0], branch))
                                   .Select(t => new GitRef(module, objectId: null, t[1]))
                                   .FirstOrDefault(h => h.IsHead);

            if (remoteHead is not null)
            {
                return remoteHead.Name;
            }

            GitRef remoteWildcardHead = remote.Push
                                   .Select(s => s.Split(Delimiters.Colon))
                                   .Where(t => t.Length == 2)
                                   .Where(t => IsSettingForWildcardBranch(t[0]))
                                   .Select(t => new GitRef(module, objectId: null, t[1].Replace("*", branch)))
                                   .FirstOrDefault(h => h.IsHead);

            return remoteWildcardHead?.Name;

            bool IsSettingForBranch(string setting, string branchName)
            {
                GitRef head = new(module, objectId: null, setting);
                return head.IsHead && head.Name.Equals(branchName, StringComparison.OrdinalIgnoreCase);
            }

            bool IsSettingForWildcardBranch(string setting)
            {
                GitRef head = new(module, objectId: null, setting);
                return head.IsHead && head.Name == "*";
            }
        }

        /// <summary>
        /// Retrieves disabled remotes from .git/config file.
        /// </summary>
        public IReadOnlyList<Remote> GetDisabledRemotes()
        {
            IGitModule module = GetModule();

            return GetDisabledRemoteNames()
                .Select(remoteName =>
                {
                    string settingPrefix = $"{SectionRemoteDisabled}.{remoteName}.";
                    string url = module.GetEffectiveSetting($"{settingPrefix}url");
                    string firstPushUrl = module.GetSettings($"{settingPrefix}pushurl").FirstOrDefault(defaultValue: url);
                    return new Remote(name: remoteName, fetchUrl: url, firstPushUrl);
                })
                .ToList();
        }

        public IReadOnlyList<string> GetDisabledRemoteNames()
        {
            IGitModule module = GetModule();

            return EnumerateDisabledRemoteNames()
                .WhereNotNull()
                .Distinct()
                .ToList();

            IEnumerable<string> EnumerateDisabledRemoteNames()
            {
                foreach ((string setting, string _) in module.GetAllLocalSettings())
                {
                    (string section, string? subsection, string _) = IGitConfigSettingsGetter.SplitSetting(setting);
                    if (section == SectionRemoteDisabled)
                    {
                        yield return subsection;
                    }
                }
            }
        }

        public IReadOnlyList<string> GetEnabledRemoteNames()
        {
            return GetModule().GetRemoteNames();
        }

        // TODO: candidate for Async implementations
        public IEnumerable<ConfigFileRemote> LoadRemotes(bool loadDisabled)
        {
            List<ConfigFileRemote> remotes = [];
            IGitModule module = _getModule();
            if (module is null)
            {
                return remotes;
            }

            PopulateRemotes(module, remotes, true);
            if (loadDisabled)
            {
                PopulateRemotes(module, remotes, false);
            }

            return remotes.OrderBy(x => x.Name);

            // pass the list in to minimise allocations
            void PopulateRemotes(IGitModule module, List<ConfigFileRemote> allRemotes, bool enabled)
            {
                Func<IReadOnlyList<string>> func;
                if (enabled)
                {
                    func = module.GetRemoteNames;
                }
                else
                {
                    func = GetDisabledRemoteNames;
                }

                List<string> gitRemotes = [.. func().Where(x => !string.IsNullOrWhiteSpace(x))];
                if (gitRemotes.Count == 0)
                {
                    return;
                }

                allRemotes.AddRange(gitRemotes.Select(remote => new ConfigFileRemote
                {
                    Disabled = !enabled,
                    Name = remote,
                    Url = module.GetSetting(GetSettingKey(SettingKeyString.RemoteUrl, remote, enabled)),
                    Push = module.GetSettings(GetSettingKey(SettingKeyString.RemotePush, remote, enabled)).ToList(),
                    Color = module.GetSetting(GetSettingKey(SettingKeyString.RemoteColor, remote, enabled)),

                    // Note: This only gets the last pushurl
                    PushUrl = module.GetSetting(GetSettingKey(SettingKeyString.RemotePushUrl, remote, enabled)),
                    PuttySshKey = module.GetSetting(GetSettingKey(SettingKeyString.RemotePuttySshKey, remote, enabled))
                }));
            }
        }

        /// <summary>
        /// Removes the specified remote from .git/config file.
        /// </summary>
        /// <param name="remote">Remote to remove.</param>
        /// <returns>Output of <see cref="IGitModule.RemoveRemote"/> operation, if the remote is active; otherwise <see cref="string.Empty"/>.</returns>
        public string RemoveRemote(ConfigFileRemote remote)
        {
            ArgumentNullException.ThrowIfNull(remote);

            IGitModule module = GetModule();
            if (!remote.Disabled)
            {
                Validates.NotNull(remote.Name);
                return module.RemoveRemote(remote.Name);
            }

            module.RemoveConfigSection(SectionRemoteDisabled, subsection: remote.Name);
            return string.Empty;
        }

        public bool EnabledRemoteExists(string remoteName)
        {
            return GetEnabledRemoteNames().Any(r => r == remoteName);
        }

        public bool DisabledRemoteExists(string remoteName)
        {
            return GetDisabledRemoteNames().Any(r => r == remoteName);
        }

        public ConfigFileRemoteSaveResult SaveRemote(ConfigFileRemote? remote, string remoteName, string remoteUrl, string? remotePushUrl, string remotePuttySshKey, string? remoteColor)
        {
            if (string.IsNullOrWhiteSpace(remoteName))
            {
                throw new ArgumentNullException(nameof(remoteName));
            }

            remoteName = remoteName.Trim();

            // if create a new remote or updated the url - we may need to perform "update remote"
            bool updateRemoteRequired = false;

            // if operation return anything back, relay that to the user
            string output = string.Empty;

            IGitModule module = GetModule();
            bool remoteDisabled = false;
            if (remote is null)
            {
                output = module.AddRemote(remoteName, remoteUrl);

                // If output was returned, something went wrong
                if (!string.IsNullOrWhiteSpace(output))
                {
                    return new ConfigFileRemoteSaveResult(output, false);
                }

                updateRemoteRequired = true;
            }
            else
            {
                if (remote.Disabled)
                {
                    // disabled branches can't updated as it poses to many problems, i.e.
                    // - verify that the branch name is valid, and
                    // - it does not duplicate an active branch name etc.
                    return new ConfigFileRemoteSaveResult(null, false);
                }

                remoteDisabled = remote.Disabled;
                if (!string.Equals(remote.Name, remoteName, StringComparison.Ordinal))
                {
                    Validates.NotNull(remote.Name);

                    // the name of the remote changed - perform rename
                    output = module.RenameRemote(remote.Name, remoteName);
                }

                if (!string.Equals(remote.Url, remoteUrl, StringComparison.Ordinal))
                {
                    // the remote url changed - we may need to update remote
                    updateRemoteRequired = true;
                }
            }

            // If URL is in a Windows path, it may need to be converted to WSL Git path
            if (remoteUrl is not null && !Uri.IsWellFormedUriString(remoteUrl, UriKind.RelativeOrAbsolute))
            {
                remoteUrl = module.GetPathForGitExecution(remoteUrl);
            }

            UpdateSettings(module, remoteName, remoteDisabled, SettingKeyString.RemoteUrl, remoteUrl);
            if (remotePushUrl is not null && !Uri.IsWellFormedUriString(remotePushUrl, UriKind.RelativeOrAbsolute))
            {
                remotePushUrl = module.GetPathForGitExecution(remotePushUrl);
            }

            UpdateSettings(module, remoteName, remoteDisabled, SettingKeyString.RemotePushUrl, remotePushUrl);
            UpdateSettings(module, remoteName, remoteDisabled, SettingKeyString.RemotePuttySshKey, remotePuttySshKey);
            UpdateSettings(module, remoteName, remoteDisabled, SettingKeyString.RemoteColor, remoteColor);

            return new ConfigFileRemoteSaveResult(output, updateRemoteRequired);
        }

        public void ToggleRemoteState(string remoteName, bool disabled)
        {
            if (string.IsNullOrWhiteSpace(remoteName))
            {
                throw new ArgumentNullException(nameof(remoteName));
            }

            // disabled is the new state, so if the new state is 'false' (=enabled), then the existing state is 'true' (=disabled, i.e. '-remote')
            string sectionName = disabled ? SectionRemote : SectionRemoteDisabled;
            string newSectionName = disabled ? SectionRemoteDisabled : SectionRemote;

            IGitModule module = GetModule();

            List<(string, string)> newSectionValues = [];
            bool newSectionAlreadyExists = false;
            foreach ((string setting, string value) in module.GetAllLocalSettings())
            {
                (string section, string? subsection, string name) = IGitConfigSettingsGetter.SplitSetting(setting);
                if (subsection != remoteName)
                {
                    continue;
                }

                if (section == newSectionName)
                {
                    newSectionAlreadyExists = true;
                    continue;
                }

                if (section != sectionName)
                {
                    continue;
                }

                string newSetting = $"{newSectionName}.{remoteName}.{name}";
                newSectionValues.Add((newSetting, value));
            }

            if (newSectionValues.Count == 0)
            {
                // we didn't find the section, nothing we can do
                return;
            }

            if (disabled)
            {
                module.RemoveRemote(remoteName);
            }
            else
            {
                module.RemoveConfigSection(sectionName, subsection: remoteName);
            }

            // ensure that the section with the same name doesn't already exist
            // use case:
            // - a user has added a remote,
            // - then deactivated the remote via GE
            // - then added a remote with the same name from a command line or via UI
            // - then attempted to deactivate the new remote
            if (newSectionAlreadyExists)
            {
                module.RemoveConfigSection(newSectionName, subsection: remoteName);
            }

            // add the renamed remote settings
            foreach ((string setting, string value) in newSectionValues)
            {
                module.SetSetting(setting, value, append: true);
            }
        }

        private IGitModule GetModule()
        {
            IGitModule module = _getModule();
            if (module is null)
            {
                throw new ArgumentException($"Require a valid instance of {nameof(IGitModule)}");
            }

            return module;
        }

        private static string GetSettingKey(string settingKey, string remoteName, bool remoteEnabled)
        {
            string key = string.Format(settingKey, remoteName);
            return remoteEnabled ? key : DisabledSectionPrefix + key;
        }

        private static void UpdateSettings(IGitModule module, string remoteName, bool remoteDisabled, string settingName, string? value)
        {
            string prefix = remoteDisabled ? DisabledSectionPrefix : string.Empty;
            string fullSettingName = prefix + string.Format(settingName, remoteName);

            if (!string.IsNullOrWhiteSpace(value))
            {
                module.SetSetting(fullSettingName, value);
            }
            else
            {
                module.UnsetSetting(fullSettingName);
            }
        }
    }
}
